Managing Transactions and Entity Relationships with Spring Data JPA
Course: Advance Java Training
Topic: Managing Transactions and Entity Relationships with Spring Data JPA
Course Link: Advance Java Training Course
Demo Link: Register for a Course Demo
JustAcademy Website: Visit JustAcademy
1. Introduction
Spring Data JPA simplifies database operations in Java applications by providing repository interfaces, entity mapping, and transaction management. In real-world applications, a single business operation may involve several database changes, such as creating an order, updating inventory, and recording a payment. Transactions help ensure that related changes are handled consistently.
Entity relationships allow Java objects to represent connections between database tables. For example, one department may contain multiple employees, or one customer may place multiple orders. JPA provides annotations to represent these relationships and manage how associated entities are stored and retrieved.
Understanding transactions and entity relationships is essential for building reliable applications using Spring Boot, Hibernate, JPA, and relational databases.
2. Learning Objectives
- Understand database transactions and the ACID properties.
- Use Spring's
@Transactional annotation correctly.
- Understand transaction propagation, rollback rules, and read-only transactions.
- Learn entity lifecycle states and persistence-context behavior.
- Implement one-to-one, one-to-many, many-to-one, and many-to-many relationships.
- Understand owning and inverse sides of entity relationships.
- Configure cascading operations and orphan removal.
- Choose suitable lazy and eager loading strategies.
- Avoid common problems such as circular references and the N+1 query issue.
- Apply transaction and relationship management best practices.
3. What Is a Database Transaction?
A database transaction is a logical unit of work containing one or more database operations. These operations are treated as a single unit: they either complete successfully according to the transaction's rules or are rolled back when a failure requires it.
For example, consider a banking application transferring ₹1,000 from Account A to Account B. The application must debit one account and credit the other. If the debit succeeds but the credit fails, the application should not leave the accounts in an inconsistent state.
Example of a Transaction
BEGIN TRANSACTION;
UPDATE accounts
SET balance = balance - 1000
WHERE id = 1;
UPDATE accounts
SET balance = balance + 1000
WHERE id = 2;
COMMIT;
This SQL illustrates the transaction concept. In a Spring Data JPA application, transaction boundaries are commonly managed with Spring's transaction management rather than manually issuing transaction statements in repository code.
4. Understanding the ACID Properties
ACID describes four important properties associated with reliable database transactions.
| Property |
Meaning |
Example |
| Atomicity |
A transaction's changes are treated as one unit. |
A transfer should not complete only its debit operation. |
| Consistency |
A transaction preserves defined database rules and constraints. |
An account must not violate an enforced balance rule. |
| Isolation |
Concurrent transactions are controlled according to the configured isolation level. |
Two users updating the same record should not silently overwrite each other's changes. |
| Durability |
Committed changes persist according to the database's durability guarantees. |
A successful transaction remains stored after a normal application restart. |
The database and transaction manager provide the underlying guarantees. Application code must still enforce business rules and handle concurrency appropriately.
5. What Is Transaction Management in Spring Data JPA?
Spring transaction management provides a consistent way to define when a transaction starts, participates in an existing transaction, commits, or rolls back. With Spring Data JPA, transaction management commonly integrates with the JPA provider, such as Hibernate, and the configured database.
The @Transactional annotation is the standard declarative mechanism for defining transaction behavior around service or repository methods.
Important Transaction Components
@Transactional: Defines transactional behavior for a method or class.
- PlatformTransactionManager: Spring abstraction for coordinating transactions.
- JpaTransactionManager: Common transaction manager for a single JPA
EntityManagerFactory.
- EntityManager: Manages persistent entities and their interaction with the persistence context.
- Spring Data JPA repository: Provides database operations such as
save(), findById(), and delete().
6. Using the @Transactional Annotation
The @Transactional annotation can be applied to a public service method or a service class to define a transaction boundary. A service method is often the best location because it represents a complete business operation that may call multiple repositories.
Example Service Class
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class StudentService {
private final StudentRepository studentRepository;
public StudentService(StudentRepository studentRepository) {
this.studentRepository = studentRepository;
}
@Transactional
public Student createStudent(Student student) {
return studentRepository.save(student);
}
}
When the method is invoked through Spring's transaction proxy, Spring applies the configured transaction behavior. If the method completes normally, the transaction can commit. If an applicable exception causes rollback, the transaction is rolled back.
Class-Level Transactions
@Service
@Transactional
public class StudentService {
private final StudentRepository studentRepository;
public StudentService(StudentRepository studentRepository) {
this.studentRepository = studentRepository;
}
public Student saveStudent(Student student) {
return studentRepository.save(student);
}
@Transactional(readOnly = true)
public List<Student> getAllStudents() {
return studentRepository.findAll();
}
}
Class-level configuration applies to eligible methods by default. A method-level annotation can override selected transaction attributes. Import java.util.List when using the example.
7. Default Transaction Behavior
Spring Data JPA's inherited CRUD repository methods generally have transaction configuration supplied by the repository implementation. Read operations are normally configured as read-only, while modifying CRUD operations use ordinary transactional settings. However, declared query methods do not automatically receive transaction configuration in every case.
For business operations involving multiple repository calls, defining the transaction boundary at the service layer is usually clearer. The outer service transaction determines the effective transaction for participating repository operations.
Important: The readOnly = true attribute is primarily a hint for the transaction infrastructure and persistence provider. It is not a universal guarantee that writes are impossible.
8. Transaction Commit and Rollback
A transaction commits when the operation completes successfully and the transaction manager determines that it can commit. A rollback discards the transactional database changes when the transaction is marked for rollback or an applicable failure occurs.
Example with Rollback
@Service
public class OrderService {
private final OrderRepository orderRepository;
private final InventoryService inventoryService;
public OrderService(
OrderRepository orderRepository,
InventoryService inventoryService) {
this.orderRepository = orderRepository;
this.inventoryService = inventoryService;
}
@Transactional
public void placeOrder(Order order) {
orderRepository.save(order);
inventoryService.reduceStock(order);
}
}
If reduceStock() participates in the same transaction and an applicable unchecked exception escapes the transactional boundary, the database changes can be rolled back together. The exact behavior depends on transaction propagation, exception handling, and the transaction manager.
Default Rollback Rules
- By default, Spring rolls back for unchecked exceptions such as
RuntimeException and for Error.
- Checked exceptions do not trigger rollback by default.
- Use
rollbackFor when a checked exception should trigger rollback.
- Catch-and-suppress exception handling may prevent the transaction interceptor from seeing the failure, unless the transaction is explicitly marked rollback-only.
Rollback for a Checked Exception
@Transactional(rollbackFor = Exception.class)
public void processOrder() throws Exception {
// Perform related database operations.
// A matching exception can trigger rollback.
}
Choose rollback rules based on the application's business requirements. Do not catch exceptions and continue as if the transaction were successful when the database may be in an inconsistent state.
9. Transaction Propagation
Propagation defines how a transactional method behaves when it is invoked while another transaction is already active.
| Propagation |
Behavior |
Common Use |
REQUIRED |
Uses the existing transaction or starts one if none exists. |
Default for most service operations. |
REQUIRES_NEW |
Suspends an existing transaction and starts an independent transaction. |
Operations that must commit independently, when appropriate. |
SUPPORTS |
Uses an existing transaction if available; otherwise executes without one. |
Operations that can work either way. |
MANDATORY |
Requires an existing transaction and fails if none exists. |
Operations that must run within a larger unit of work. |
NOT_SUPPORTED |
Suspends an existing transaction and executes without one. |
Operations that should not run in a transaction. |
NEVER |
Fails if an existing transaction is active. |
Operations that must not participate in a transaction. |
NESTED |
Attempts a nested transaction using a savepoint when supported by the transaction manager. |
Specialized workflows that require savepoint behavior. |
Example of REQUIRES_NEW
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void saveAuditRecord(AuditRecord record) {
auditRepository.save(record);
}
This method requests a separate transaction. If an outer transaction is active, Spring suspends it while the new transaction runs. The independent transaction can commit even if the outer transaction later rolls back, provided the new transaction itself succeeds.
Use REQUIRES_NEW carefully because it can require an additional database connection and may create behavior that is surprising if an outer operation fails.
10. Transaction Isolation Levels
Isolation controls how concurrent transactions can observe each other's changes. The exact behavior depends on the database, transaction manager, and supported isolation levels.
| Isolation Level |
General Meaning |
DEFAULT |
Uses the database or transaction manager's default isolation. |
READ_UNCOMMITTED |
May permit dirty reads where supported. |
READ_COMMITTED |
Prevents dirty reads, but repeated reads may return different committed values. |
REPEATABLE_READ |
Provides stronger repeat-read guarantees; phantom behavior depends on the database. |
SERIALIZABLE |
Provides the strongest standard isolation level, potentially reducing concurrency. |
Setting an Isolation Level
@Transactional(isolation = Isolation.READ_COMMITTED)
public void updateStudent(Student student) {
studentRepository.save(student);
}
Isolation settings matter most when concurrent transactions read and update overlapping data. Higher isolation can improve consistency but may reduce throughput or cause more blocking. Confirm the behavior using your database's documentation.
11. What Is an Entity Relationship?
An entity relationship describes how two or more entity types are associated. JPA uses relationship annotations to represent these associations in Java while mapping them to foreign keys, join tables, or other relational structures.
For example, a college system may contain:
- A
Department entity representing an academic department.
- An
Employee entity representing a staff member.
- A relationship indicating that each employee belongs to a department.
JPA supports four common relationship types:
@OneToOne
@OneToMany
@ManyToOne
@ManyToMany
12. Understanding @OneToOne Relationships
A one-to-one relationship connects one record in an entity to at most one record in another entity. For example, a user may have one profile, and each profile may belong to one user.
Example: User and UserProfile
@Entity
public class UserProfile {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String address;
@OneToOne(mappedBy = "profile")
private User user;
public UserProfile() {
}
}
@Entity
@Table(name = "app_users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String username;
@OneToOne(cascade = CascadeType.ALL)
@JoinColumn(name = "profile_id", unique = true)
private UserProfile profile;
public User() {
}
}
In this example, the User entity owns the relationship because it defines the @JoinColumn. The unique constraint helps enforce one-to-one cardinality in the database. Production schemas should also consider whether the relationship must be mandatory or optional.
13. Understanding @OneToMany Relationships
A one-to-many relationship connects one entity to multiple related entities. For example, one department can have many employees.
Example: Department and Employees
@Entity
public class Department {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@OneToMany(
mappedBy = "department",
cascade = CascadeType.ALL,
orphanRemoval = true
)
private List<Employee> employees = new ArrayList<>();
public Department() {
}
public void addEmployee(Employee employee) {
employees.add(employee);
employee.setDepartment(this);
}
public void removeEmployee(Employee employee) {
employees.remove(employee);
employee.setDepartment(null);
}
}
@Entity
public class Employee {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "department_id")
private Department department;
public Employee() {
}
public void setDepartment(Department department) {
this.department = department;
}
}
The Employee table normally stores the foreign key in department_id. The Department.employees collection is the inverse side because it uses mappedBy = "department".
The helper methods update both sides of the relationship. This is important because JPA uses the owning side to determine database relationship changes.
14. Understanding @ManyToOne Relationships
A many-to-one relationship means many records in one entity can refer to one record in another entity. For example, multiple employees can belong to the same department.
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "department_id")
private Department department;
The @ManyToOne relationship is commonly the owning side because it contains the foreign key. Setting an employee's department updates the relationship when the entity is persisted or changed within a transaction.
Example Service Method
@Transactional
public Employee assignEmployeeToDepartment(
Long employeeId,
Long departmentId) {
Employee employee = employeeRepository.findById(employeeId)
.orElseThrow(() -> new IllegalArgumentException(
"Employee not found"));
Department department = departmentRepository.findById(departmentId)
.orElseThrow(() -> new IllegalArgumentException(
"Department not found"));
employee.setDepartment(department);
return employeeRepository.save(employee);
}
Because the entity is managed inside the transaction, changing its department is normally detected by JPA's dirty-checking mechanism. Calling save() can still be useful for consistency with the repository abstraction, but it is not always necessary for an already managed entity.
15. Understanding @ManyToMany Relationships
A many-to-many relationship allows multiple records on each side to be associated with multiple records on the other side. For example, one student may enroll in multiple courses, and each course may contain multiple students.
Example: Student and Course
@Entity
public class Student {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@ManyToMany
@JoinTable(
name = "student_courses",
joinColumns = @JoinColumn(name = "student_id"),
inverseJoinColumns = @JoinColumn(name = "course_id")
)
private Set<Course> courses = new HashSet<>();
public Student() {
}
}
@Entity
public class Course {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String title;
@ManyToMany(mappedBy = "courses")
private Set<Student> students = new HashSet<>();
public Course() {
}
}
The student_courses join table stores pairs of student and course identifiers. The Student entity owns the relationship because it defines @JoinTable.
In more complex systems, consider replacing a many-to-many association with a separate enrollment entity when the relationship needs additional attributes such as enrollment date, status, grade, or payment details.
16. Owning Side and Inverse Side
In a bidirectional relationship, one entity is the owning side and the other is the inverse side. The owning side determines the database relationship mapping. The inverse side refers to the owning field through mappedBy.
| Relationship |
Common Owning Side |
Inverse Side |
| One-to-one |
Entity containing @JoinColumn |
Entity using mappedBy |
| One-to-many / many-to-one |
Many-to-one entity containing the foreign key |
One-to-many collection using mappedBy |
| Many-to-many |
Entity containing @JoinTable |
Entity using mappedBy |
Updating only the inverse side may not persist the intended relationship. Use helper methods that keep both sides synchronized, and ensure the owning side is updated before committing the transaction.
17. Cascade Operations in JPA
Cascade operations allow certain persistence operations on a parent entity to propagate to its associated entities. They are configured using the cascade attribute of a relationship annotation.
| Cascade Type |
Purpose |
PERSIST |
Propagates persistence of a new entity. |
MERGE |
Propagates merge operations. |
REMOVE |
Propagates removal operations. |
REFRESH |
Propagates refresh operations. |
DETACH |
Propagates detachment from the persistence context. |
ALL |
Includes all the cascade types above. |
Example of Cascading Persistence
@OneToMany(
mappedBy = "department",
cascade = CascadeType.PERSIST
)
private List<Employee> employees = new ArrayList<>();
When a new department is persisted, new employees associated with it can also be persisted through the cascade. Cascading should match the lifecycle relationship between the objects.
Warning: Avoid using CascadeType.ALL automatically on every relationship. For example, deleting a department should not necessarily delete employees if those employees must remain in the system or be reassigned.
18. Orphan Removal
The orphanRemoval = true setting tells JPA to remove a child entity when it is removed from the relevant parent association, according to the relationship's orphan-removal semantics.
@OneToMany(
mappedBy = "department",
cascade = CascadeType.ALL,
orphanRemoval = true
)
private List<Employee> employees = new ArrayList<>();
For example, removing an employee from the department's managed collection can cause that employee to be deleted when the persistence context is synchronized with the database.
Use orphan removal only when the child is genuinely dependent on the parent. It is not the same as setting the foreign key to null, and it should not be used when the child must remain independently available.
19. Lazy and Eager Loading
Fetch type determines when related entities are loaded. The choice affects performance, database access patterns, and how the application accesses relationships.
| Fetch Type |
Behavior |
Considerations |
LAZY |
Related data is fetched when needed, subject to provider behavior. |
Can avoid unnecessary loading, but accessing data after the persistence context closes may fail. |
EAGER |
Related data must be available eagerly when the entity is loaded. |
May cause additional queries or load more data than the use case requires. |
JPA defaults commonly specify eager loading for @ManyToOne and @OneToOne, and lazy loading for @OneToMany and @ManyToMany. Explicitly choosing LAZY for relationships that do not always need to be loaded is often a useful starting point, but the final fetch plan should match actual application needs.
Example
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "department_id")
private Department department;
When the department is accessed, the provider may issue an additional SQL query. Keep access within a suitable persistence-context boundary or load the required data explicitly.
20. Understanding the Persistence Context and Entity States
The persistence context is the set of managed entity instances associated with an EntityManager. JPA tracks changes to managed entities and synchronizes those changes with the database during a flush, commonly before transaction commit.
| Entity State |
Description |
| Transient |
A newly created object that is not yet managed or stored by the persistence context. |
| Managed |
An entity tracked by the persistence context. |
| Detached |
An entity that was managed but is no longer attached to that persistence context. |
| Removed |
A managed entity marked for deletion. |
Example of Dirty Checking
@Transactional
public void updateEmployeeName(Long id, String newName) {
Employee employee = employeeRepository.findById(id)
.orElseThrow(() -> new IllegalArgumentException(
"Employee not found"));
employee.setName(newName);
}
Because the employee is managed within the transaction, JPA detects the changed name and normally writes the update when the persistence context is flushed. An explicit call to save() is not required for this managed-entity change, although it may be used for consistency with repository conventions.
21. Managing Multiple Repository Operations in One Transaction
A single business operation may involve several repositories. Defining a transaction at the service layer lets the related database changes participate in the same transaction.
Example: Create a Department and Its Employees
@Service
public class DepartmentService {
private final DepartmentRepository departmentRepository;
public DepartmentService(
DepartmentRepository departmentRepository) {
this.departmentRepository = departmentRepository;
}
@Transactional
public Department createDepartmentWithEmployees(
Department department,
List<Employee> employees) {
for (Employee employee : employees) {
department.addEmployee(employee);
}
return departmentRepository.save(department);
}
}
This example assumes that the relationship is configured with appropriate cascade behavior and that addEmployee() updates both sides. If the operation fails with an exception that triggers rollback, the related database changes can be rolled back together.
22. Preventing the N+1 Query Problem
The N+1 query problem occurs when an application first loads a collection of entities and then executes an additional query for each entity to load related data. For example, loading ten departments and separately loading employees for each department may cause one query for departments and ten more queries for employees.
Common Solutions
- Use JPQL fetch joins when related data is needed for a particular use case.
- Use entity graphs to specify which relationships should be loaded.
- Use DTO projections for read-only screens that need selected fields.
- Review SQL logs and database query plans to identify unnecessary queries.
- Consider batch fetching when supported by the persistence provider and suitable for the workload.
Example of a Fetch Join
@Query("SELECT DISTINCT d FROM Department d LEFT JOIN FETCH d.employees")
List<Department> findAllDepartmentsWithEmployees();
The fetch join requests department and employee data together. The DISTINCT keyword helps avoid duplicate department entities in the query result when multiple employees match the same department.
Be cautious when combining collection fetch joins with pagination. A collection join can multiply result rows and make pagination inaccurate or inefficient. Consider a two-step loading strategy or DTO-based query for paginated results.
23. Avoiding LazyInitializationException
A LazyInitializationException can occur when an application tries to access an uninitialized lazy relationship after its persistence context has closed. This often happens when entities are returned from a service and their relationships are accessed later by a controller or serialization layer.
Recommended Solutions
- Load the required relationship inside the service transaction.
- Use a fetch join or entity graph for the specific query.
- Return DTOs containing the data required by the client.
- Avoid making every relationship eager simply to suppress the exception.
- Avoid relying on an open persistence context throughout the web request as a substitute for a deliberate data-fetching strategy.
Example with DTO Mapping
@Transactional(readOnly = true)
public DepartmentSummary getDepartmentSummary(Long id) {
Department department = departmentRepository.findById(id)
.orElseThrow(() -> new IllegalArgumentException(
"Department not found"));
int employeeCount = department.getEmployees().size();
return new DepartmentSummary(
department.getId(),
department.getName(),
employeeCount
);
}
In this example, DepartmentSummary is a DTO that should define a matching constructor and getters or other suitable accessors. If the employee collection is large, use a dedicated count query rather than loading every employee just to calculate the count.
24. Handling Bidirectional Relationships in REST APIs
Bidirectional entity relationships can create circular object graphs. For example, a department contains employees, while each employee references its department. Directly serializing both sides can lead to recursive JSON output or serialization errors.
Common approaches include:
- Use DTOs to control the shape of API responses.
- Return identifiers or summaries for related entities when full objects are unnecessary.
- Use serialization annotations when their behavior is well understood and appropriate.
- Avoid exposing JPA entities directly as public API contracts in complex applications.
25. Optimistic and Pessimistic Locking
When multiple transactions update the same record, locking can help prevent lost updates or coordinate access to shared data.
Optimistic Locking
Optimistic locking uses a version field to detect whether another transaction changed the entity before the current transaction's update is completed.
@Version
private Long version;
JPA checks the version during updates. If a conflicting update is detected, the transaction may fail with an optimistic locking exception. The application can handle the conflict by asking the user to retry, reloading the latest data, or applying a business-specific resolution.
Pessimistic Locking
Pessimistic locking requests a database lock to coordinate concurrent access to a record.
import jakarta.persistence.LockModeType;
import org.springframework.data.jpa.repository.Lock;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("SELECT i FROM Inventory i WHERE i.id = :id")
Optional<Inventory> findByIdForUpdate(
@Param("id") Long id);
Use locking only when the concurrency requirements justify it. Pessimistic locks can block other transactions and may contribute to deadlocks if multiple records are locked in inconsistent orders.
26. Common Transaction and Relationship Errors
| Error or Problem |
Possible Cause |
Solution |
| Transaction does not roll back |
Exception type does not match default rollback rules, or it was caught and suppressed. |
Review exception handling and configure rollbackFor if required. |
| Changes are not persisted |
Entity is detached or the owning side of a relationship was not updated. |
Check entity state and update the owning side. |
| LazyInitializationException |
Lazy data is accessed after the persistence context closes. |
Fetch the required data within a transaction or map to a DTO. |
| Unexpected child deletion |
Cascade remove or orphan removal is too broad. |
Review entity lifecycle rules and cascade settings. |
| Duplicate relationship rows |
Join table constraints or collection handling are inadequate. |
Review collection types, relationship mapping, and database constraints. |
| N+1 query problem |
Related data is fetched separately for many entities. |
Use fetch joins, entity graphs, DTOs, or batch fetching as appropriate. |
| Concurrent update conflict |
Two transactions update the same entity. |
Consider optimistic or pessimistic locking. |
| Self-invocation bypasses transaction advice |
A method invokes another transactional method on the same object. |
Move the transactional operation to another Spring bean or redesign the call path. |
| Unexpected transaction participation |
Propagation settings differ from the intended business operation. |
Review propagation and the transaction boundary. |
27. Best Practices for Transaction Management
- Place transaction boundaries around complete business operations, usually in the service layer.
- Keep transactions reasonably short to reduce lock duration and connection usage.
- Use
@Transactional(readOnly = true) for suitable read-only service operations.
- Choose rollback rules deliberately, especially when checked exceptions are involved.
- Do not perform slow external network calls while holding database transactions open unless the design requires it.
- Use optimistic locking for appropriate concurrent-update scenarios.
- Understand that a transaction on one database does not automatically make an external HTTP call or message delivery atomic.
- For workflows involving external services, consider an outbox pattern, retries, idempotency, or a saga-style design.
- Test transaction behavior using integration tests with a real or representative database.
28. Best Practices for Entity Relationships
- Model relationships according to actual business rules and database constraints.
- Keep both sides synchronized in bidirectional relationships.
- Use
mappedBy correctly to identify the inverse side.
- Choose cascade operations according to entity lifecycle ownership.
- Use orphan removal only when a child should not exist independently of its parent association.
- Prefer lazy loading for relationships that are not always needed, while defining explicit fetch plans for use cases.
- Use DTOs to prevent circular serialization and reduce unnecessary data transfer.
- Review generated SQL and test for N+1 queries.
- Use database foreign keys, uniqueness constraints, and not-null constraints where appropriate.
- Use locking or version checks where concurrent changes can cause data loss.
29. Practical Project: Department and Employee Management
Create a Spring Boot application to manage departments and employees while applying transaction management and JPA relationships.
Project Requirements
- Create a
Department entity with an identifier and department name.
- Create an
Employee entity with an identifier, name, salary, and department relationship.
- Implement a one-to-many relationship from department to employees.
- Implement a many-to-one relationship from employee to department.
- Create repository interfaces for both entities.
- Build a service method to create a department with multiple employees in one transaction.
- Build a method to assign an employee to an existing department.
- Configure cascading behavior only where the department owns the lifecycle of the employee records.
- Retrieve departments with employee data without generating unnecessary queries.
- Write integration tests for commit, rollback, relationship persistence, and deletion behavior.
Example Repository Interfaces
import org.springframework.data.jpa.repository.JpaRepository;
public interface DepartmentRepository
extends JpaRepository<Department, Long> {
}
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
public interface EmployeeRepository
extends JpaRepository<Employee, Long> {
List<Employee> findByDepartmentId(Long departmentId);
}
Complete the project by adding validation, exception handling, REST controllers, and tests. Confirm that the foreign key is stored correctly and that failed transactional operations do not leave partial database changes.
30. Interview Questions and Answers
Q1. What is transaction management in Spring Data JPA?
It controls how database operations begin, participate in transactions, commit, and roll back, helping related operations maintain consistency.
Q2. What does @Transactional do?
It declares transactional behavior for eligible methods or classes managed through Spring's transaction infrastructure.
Q3. What is the default propagation behavior?
The default propagation is REQUIRED, which joins an existing transaction or starts one if none exists.
Q4. When does Spring roll back a transaction by default?
By default, unchecked exceptions and errors trigger rollback. Checked exceptions do not by default, unless rollback rules are configured.
Q5. What is the difference between one-to-many and many-to-one?
One-to-many describes one parent associated with multiple children. Many-to-one describes multiple child records referencing one parent.
Q6. What is the owning side of a relationship?
It is the side that controls the relationship mapping in the database, typically the side containing the foreign key or join-table definition.
Q7. What is cascade in JPA?
Cascade propagates specified persistence operations from one entity to associated entities.
Q8. What is orphanRemoval?
It allows JPA to remove a child entity when it is removed from the parent association under the configured orphan-removal semantics.
Q9. What is the difference between lazy and eager loading?
Lazy loading fetches related data when needed, while eager loading requires related data to be available eagerly.
Q10. What is the N+1 query problem?
It occurs when an initial query retrieves a set of entities and additional queries are executed for each entity's related data.
Q11. What is optimistic locking?
Optimistic locking uses a version or similar mechanism to detect conflicting updates instead of locking the record for the entire operation.
Q12. Why are transactions often defined at the service layer?
A service method represents a business operation that may involve multiple repositories, making it a natural boundary for atomic database work.
31. Quick Revision Checklist
- Understand ACID transaction properties.
- Use
@Transactional to define service-level transaction boundaries.
- Know the default rollback rules and when to use
rollbackFor.
- Understand propagation and isolation settings.
- Recognize transient, managed, detached, and removed entity states.
- Understand all four JPA relationship types.
- Know the owning side, inverse side, and purpose of
mappedBy.
- Use cascade and orphan removal only when they match the intended lifecycle.
- Plan lazy loading and avoid N+1 queries.
- Use DTOs for suitable API responses and explicit data-fetching strategies.
- Consider optimistic or pessimistic locking for concurrent updates.
- Write integration tests for rollback and relationship behavior.
32. Conclusion
Managing transactions and entity relationships is a fundamental skill in Spring Data JPA development. Transactions allow multiple database operations to participate in a consistent unit of work, while relationship mappings connect Java entities to the structure of a relational database.
By using @Transactional thoughtfully, understanding rollback rules, choosing appropriate cascade and fetch strategies, and keeping both sides of bidirectional relationships synchronized, developers can create reliable and maintainable applications. Performance testing, integration testing, and careful concurrency management help ensure these applications behave correctly under real-world conditions.
Continue Learning: Explore the Advance Java Training course at JustAcademy to build your Java and Spring development skills.
Course Demo: Register for a JustAcademy course demo.
Official Documentation: Spring Data JPA Transactionality Documentation.
Visit: JustAcademy Official Website.